Skip to main content

API reference

Every public type and function in hijri.h. All angles are in degrees, all distances as noted per field, and all instants are Julian Days unless a name says otherwise.

Time scales

Julian Day arguments are UT unless the parameter name ends in _tt. The distinction is not cosmetic: the two differ by delta T, which is about 69 seconds today and grows for dates far from the present.

double hijri_delta_t_seconds(double jd);
double hijri_jd_tt_from_ut(double jd_ut);

The position functions take TT, the event finders take UT and convert internally.

Calendar arithmetic

double hijri_jd_from_gregorian(int year, int month, double day);
void hijri_gregorian_from_jd(double jd, int *year, int *month, double *day);
int hijri_jd_weekday(double jd); /* 0 = Sunday ... 6 = Saturday */

day is a double so a fraction can carry a time of day. Noon on the 27th is 27.5.

Tabular calendar

typedef struct {
int year;
int month; /* 1 = Muharram ... 12 = Dhu al-Hijjah */
int day; /* 1..30 */
} HijriDate;

double hijri_tabular_to_jd(HijriDate date);
HijriDate hijri_tabular_from_jd(double jd);
int hijri_tabular_date_valid(HijriDate date);

A fixed 30-year leap cycle, sometimes called the Kuwaiti algorithm. No astronomy, no observer, always succeeds, and reproducible on any machine. It can differ from an observed calendar by a day or two.

hijri_tabular_to_jd() and hijri_tabular_from_jd() do not validate their input. Garbage in, garbage out on an invalid month. Call hijri_tabular_date_valid() first if the date came from a user.

Location

typedef struct {
double latitude_deg;
double longitude_deg;
double elevation_m;
const char *name; /* optional, for logging */
} HijriLocation;

Longitude is positive east. name is never read by the library.

Sunset conventions

Sunset is not one definition. These pin refraction at the horizon and the solar semidiameter, which together set how far below the geometric horizon the Sun's centre sits at the moment called sunset.

typedef struct {
double refraction_at_horizon_deg;
double solar_semidiameter_arcsec_at_1au;
} HijriSunsetConvention;
ConstantRefractionSemidiameter
HIJRI_SUNSET_CONVENTION_MUHAMMADIYAH0.575959.63
HIJRI_SUNSET_CONVENTION_KEMENAG0.575959.63
HIJRI_SUNSET_CONVENTION_ASTRONOMICAL0.5667959.63

Muhammadiyah and Kemenag are numerically identical today. They are separate constants because they are separate sources, and either could change without the other.

Pass the convention that matches the criterion you are evaluating. Mixing a Kemenag threshold with the astronomical convention gives a number that belongs to neither.

Sun and Moon positions

HijriSunPosition hijri_sun_position(double jd_tt);
HijriMoonPosition hijri_moon_position(double jd_tt);
void hijri_moon_topocentric(const HijriMoonPosition *geo, double jd_ut,
double latitude_deg, double longitude_deg,
double elevation_m,
double *ra_topo_deg, double *dec_topo_deg);

:::note The two bodies are in different frames

hijri_sun_position() returns apparent coordinates, referred to the true equinox of date, with nutation and aberration applied. hijri_moon_position() returns geometric coordinates, mean equinox of date, with neither.

This is deliberate and documented in the header. Making the frames consistent was measured and made elongation accuracy slightly worse, because the mismatch partially cancels the solar truncation error. Read the header's ACCURACY CAVEAT before acting on this.

:::

Measured against JPL DE440 over 24 epochs spanning 1900 to 2100:

Moon longitude 0.0051 deg latitude 0.0006 deg distance 41.9 km
Sun longitude 0.0084042 deg

The Sun is the dominant term in the error bar, not the Moon.

Altitudes and event finders

double hijri_sun_altitude(double jd_ut, const HijriLocation *loc);
double hijri_moon_altitude(double jd_ut, const HijriLocation *loc);

HijriEventStatus hijri_find_sunset(double jd_local_midnight_ut,
const HijriLocation *loc,
const HijriSunsetConvention *conv,
double *result_jd);
HijriEventStatus hijri_find_moonset(double jd_after,
const HijriLocation *loc,
double *result_jd);

HijriEventStatus hijri_find_previous_conjunction(double jd_before, double *result_jd);
HijriEventStatus hijri_find_next_conjunction(double jd_after, double *result_jd);
HijriEventStatus hijri_find_relevant_conjunction(double jd_evening, double *result_jd);

There is no sunrise finder. Only sunset is solved, because only sunset matters to a lunar calendar.

typedef enum {
HIJRI_EVENT_OK = 0,
HIJRI_EVENT_NEVER_RISES,
HIJRI_EVENT_NEVER_SETS,
HIJRI_EVENT_NOT_FOUND
} HijriEventStatus;

Always check the status before reading *result_jd. On anything other than HIJRI_EVENT_OK the out-parameter is left untouched, so an unchecked read gives you whatever was already in your variable. The circumpolar statuses come from the rise and set solvers, and HIJRI_EVENT_NOT_FOUND comes from the conjunction finders, from non-finite input, and from hijri_find_moonset.

:::caution HIJRI_EVENT_NOT_FOUND is a normal moonset result

Since hijri.h v0.1.1 this is not a reserved status. hijri_find_moonset scans 24 hours from sunset, and the Moon sets once per 24h 50m, so roughly one evening in thirty holds no moonset at all. That is true at every latitude, Jakarta and Mecca included, and it is not a polar effect: a 24 hour window with no moonset in it is simply not the same thing as a circumpolar Moon.

Until v0.1.1 both set finders classified a failed search from one extra sample and had to answer with a circumpolar status, which produced 820 false moonset labels over latitude -89 to 89 across 2026. They now report what actually happened. If you switch exhaustively on HijriEventStatus, add the HIJRI_EVENT_NOT_FOUND arm before upgrading. :::

Evening parameters

The layer most applications want. One call computes every quantity the criteria threshold, for one evening at one location.

HijriEveningParameters hijri_compute_evening_parameters(
int gy, int gm, int gd,
const HijriLocation *loc,
const HijriSunsetConvention *conv);
FieldMeaning
jd_sunset_utSunset instant
jd_moonset_utMoonset instant
jd_relevant_conjunction_utThe conjunction this evening is judged against
sunset_status, moonset_statusCheck these first
sun_center_geometric_altitude_degSun centre, geometric
moon_center_geometric_altitude_degMoon centre, geometric
moon_upper_limb_apparent_altitude_degMoon upper limb, apparent
geocentric_elongation_degSun to Moon separation, geocentric
topocentric_elongation_degSame, corrected for parallax at the observer
moon_age_hoursSigned hours from conjunction to sunset
lag_time_minutesMoonset minus sunset
conjunction_before_sunset0 or 1
moonset_after_sunset0 or 1

The altitude fields differ in ways that matter. A criterion written for the upper limb cannot be evaluated with the centre altitude and vice versa. The gap is about 0.83 deg, since it carries the Moon's semidiameter, refraction at the horizon and parallax together, which is larger than several of the thresholds below. Measured at Jakarta on one evening, the centre sits at -1.70 deg while the upper limb sits at -0.87 deg.

Elongation comes in both geocentric and topocentric forms. MABIMS 2021 uses the topocentric value. Kemenag's own published announcements use the geocentric one, which the library established by matching a published figure to within 0.43 arcmin.

Predicates

typedef enum {
HIJRI_PREDICATE_MABIMS_1992,
HIJRI_PREDICATE_MABIMS_2021,
HIJRI_PREDICATE_WUJUDUL_HILAL,
HIJRI_PREDICATE_LAG_AT_LEAST_5_MINUTES,
HIJRI_PREDICATE_ALTITUDE_5_ELONGATION_8,
HIJRI_PREDICATE_CONJUNCTION_AND_MOONSET
} HijriLocalPredicate;

int hijri_local_predicate_evaluate(HijriLocalPredicate predicate,
const HijriEveningParameters *p);

Returns 1 if the criterion passes. The published thresholds are exposed as macros, because they are part of each criterion's definition:

#define HIJRI_MABIMS_1992_ALTITUDE_DEG 2.0
#define HIJRI_MABIMS_1992_ELONGATION_DEG 3.0
#define HIJRI_MABIMS_1992_AGE_HOURS 8.0
#define HIJRI_MABIMS_2021_ALTITUDE_DEG 3.0
#define HIJRI_MABIMS_2021_ELONGATION_DEG 6.4
#define HIJRI_WUJUDUL_HILAL_LIMB_DEG 0.0
#define HIJRI_LAG_THRESHOLD_MINUTES 5.0
#define HIJRI_RESEARCH_ALTITUDE_DEG 5.0
#define HIJRI_RESEARCH_ELONGATION_DEG 8.0

Decision margins

HijriDecisionMargins hijri_predicate_margins(HijriLocalPredicate predicate,
const HijriEveningParameters *p);

typedef struct {
HijriDecisionTerm term;
HijriUnit unit; /* HIJRI_UNIT_DEGREES, _HOURS, _MINUTES */
double value;
double threshold;
double margin; /* value - threshold */
int strict; /* 1 if the criterion uses >, 0 if >= */
int passes;
} HijriDecisionTermMargin;

Reports how far each term sits from its own threshold and nothing else. It combines nothing, converts nothing between units, and labels nothing as "near".

strict is per term rather than global because whether a value exactly on the threshold passes is a convention each criterion states, not a measurement. Singapore's MUIS, for instance, words the MABIMS 2021 thresholds as exceed, a strict >, while the library evaluates >=. That difference only bites at exact equality, but if you need MUIS wording, this field is how you implement it.

Read a margin against the right side of the error bar. The solar residual is biased rather than symmetric, measured at mean 0.0020391 deg, minimum -0.0024953 deg and maximum 0.0084042 deg, so a symmetric tolerance is the wrong comparison.

Calendar conversion

HijriMonthDecision hijri_evaluate_evening(int gy, int gm, int gd,
const HijriLocation *loc,
HijriLocalPredicate predicate);

int hijri_from_gregorian_with_local_predicate(int gy, int gm, int gd,
const HijriLocation *loc,
HijriLocalPredicate predicate,
HijriDate *out);

The conversion requires an observer location. That is a deliberate API decision, not an inconvenience: a predicate-derived Hijri date does not exist without one.

Returns 0 on failure, leaving *out untouched.

Umm al-Qura

int hijri_umm_al_qura_from_gregorian(int gy, int gm, int gd, HijriDate *out);
int hijri_umm_al_qura_covers(int gy, int gm, int gd);

Backed by the published Umm al-Qura table rather than recomputed. This is deliberate. Astronomical reconstruction reached 183 of 198 month starts, and 198 of 198 was proven unreachable from the published rule, because for seven months in the window the official table departs from its own stated criterion.

Call hijri_umm_al_qura_covers() first. Outside the table's range the conversion fails rather than extrapolating.

Visibility models

Unlike the predicates, these grade visibility into zones rather than returning a yes or no.

double hijri_yallop_q(double arcv_deg, double crescent_width_arcmin);
HijriYallopZone hijri_yallop_classify(double arcv_deg, double crescent_width_arcmin);

double hijri_odeh_v(double arcv_deg, double crescent_width_arcmin);
HijriOdehZone hijri_odeh_classify(double arcv_deg, double crescent_width_arcmin);
typedef enum {
HIJRI_YALLOP_A_EASILY_VISIBLE,
HIJRI_YALLOP_B_VISIBLE_PERFECT_CONDITIONS,
HIJRI_YALLOP_C_MAY_NEED_OPTICAL_AID,
HIJRI_YALLOP_D_NEEDS_OPTICAL_AID,
HIJRI_YALLOP_E_NOT_VISIBLE_TELESCOPE,
HIJRI_YALLOP_F_NOT_VISIBLE_BELOW_LIMIT
} HijriYallopZone;

typedef enum {
HIJRI_ODEH_NOT_VISIBLE = 0,
HIJRI_ODEH_VISIBLE_WITH_OPTICAL_AID_ONLY,
HIJRI_ODEH_VISIBLE_WITH_OPTICAL_AID_COULD_BE_NAKED_EYE,
HIJRI_ODEH_VISIBLE_NAKED_EYE
} HijriOdehZone;

The result structs carry the intermediate quantities alongside the zone, so you can show the working:

typedef struct {
double jd_best_time_ut;
double arcv_deg;
double crescent_width_arcmin;
double q; /* v, for the Odeh struct */
HijriYallopZone zone;
} HijriYallopResult;

Yallop is validated against observations. All 271 evening observations in Technical Note 69 Table 4 are checked, with a maximum q residual of 0.053577.

Odeh is not. Its coefficients and thresholds are verified verbatim against the primary source and its topocentric frame confirmed by a published anchor, but no observation-level validation exists, because the paper's Table VI is image-only. Tracked as libmuslim#47.

A zone is not a date. Turning a graded zone into a calendar decision needs an application policy that this header deliberately does not supply.

What this header will not do

  • It does not aggregate a decision across a territory
  • It does not model an official itsbat meeting or sighting testimony
  • It does not decide religious validity
  • It has no general policy abstraction, by design, until at least two complete policies with genuinely different decision flows exist

See the ROADMAP for what is planned and what is deliberately not.